// Copyright 2022 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

// ignore_for_file: avoid_print

import 'dart:io';
import 'dart:typed_data';

import 'package:collection/collection.dart';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:image/image.dart';
import 'package:integration_test/integration_test_driver_extended.dart';

const _goldensDirectoryPath = 'integration_test/test_infra/goldens';
const _failuresDirectoryPath = '$_goldensDirectoryPath/failures';
const _defaultDiffPercentage = 1.0;
const _defaultDiffTolerance = 0.003;
// The following timeouts were copied from
// https://developer.mozilla.org/en-US/docs/Web/WebDriver/Reference/Timeouts
const _defaultWebDriverPageLoadTimeoutInMins = 5;
const _defaultWebDriverScriptTimeoutInSeconds = 30;

Future<void> main() async {
  final driver = await FlutterDriver.connect();

  // Double the WebDriver timeout limits to handle test flakiness.
  // See https://github.com/flutter/devtools/issues/9474
  await driver.webDriver.timeouts.setScriptTimeout(
    const Duration(seconds: _defaultWebDriverScriptTimeoutInSeconds * 2),
  );
  await driver.webDriver.timeouts.setPageLoadTimeout(
    const Duration(minutes: _defaultWebDriverPageLoadTimeoutInMins * 2),
  );

  await integrationDriver(
    driver: driver,
    onScreenshot:
        (
          String screenshotName,
          List<int> screenshotBytes, [
          Map<String, Object?>? args,
        ]) async {
          final shouldUpdateGoldens = args?['update_goldens'] == true;

          // TODO(https://github.com/flutter/flutter/issues/118470): remove this.
          // We need this to ensure all golden image checks run. Without this
          // workaround, the flutter integration test framework will crash on the
          // failed expectation.
          final lastScreenshot = args?['last_screenshot'] == true;

          final goldenFile = File('$_goldensDirectoryPath/$screenshotName.png');

          if (shouldUpdateGoldens) {
            if (!goldenFile.existsSync()) {
              // Create the goldens directory if it does not exist.
              Directory(_goldensDirectoryPath).createSync();
            }
            goldenFile.writeAsBytesSync(screenshotBytes, flush: true);
            print('Golden image updated: $screenshotName.png');
            return true;
          }

          bool equal = false;
          double percentDiff = 1.0;
          if (goldenFile.existsSync()) {
            final goldenBytes = goldenFile.readAsBytesSync();
            equal = const DeepCollectionEquality().equals(
              goldenBytes,
              screenshotBytes,
            );
            if (!equal) {
              percentDiff = _percentDiff(goldenBytes, screenshotBytes);
            }
          }

          final failuresDirectory = Directory(_failuresDirectoryPath);

          if (!equal) {
            final percentDiffDisplay =
                '${(percentDiff * 100).toStringAsFixed(4)}%';
            if (percentDiff < _defaultDiffTolerance) {
              print(
                'Warning: $screenshotName.png differed from the golden image by '
                '$percentDiffDisplay. Since this is less than the acceptable '
                'tolerance ${(_defaultDiffTolerance * 100).toStringAsFixed(4)}%, '
                'the test still passes.',
              );
              return true;
            }
            print(
              'Golden image test failed: $screenshotName.png. The test image '
              'differed from the golden image by $percentDiffDisplay.',
            );

            // Create the goldens and failures directories if they do not exist.
            Directory(_goldensDirectoryPath).createSync();
            failuresDirectory.createSync();

            File(
              '$_failuresDirectoryPath/$screenshotName.png',
            ).writeAsBytesSync(screenshotBytes);
          }

          if (lastScreenshot &&
              failuresDirectory.existsSync() &&
              failuresDirectory.listSync().isNotEmpty) {
            return false;
          }

          return true;
        },
  );
}

double _percentDiff(Uint8List goldenBytes, List<int> screenshotBytes) {
  final goldenImage = decodeImage(goldenBytes);
  final screenshotImage = decodeImage(Uint8List.fromList(screenshotBytes));
  if (goldenImage == null || screenshotImage == null) {
    print('Cannot decode one or both of the golden images.');
    return _defaultDiffPercentage;
  }

  if (goldenImage.height != screenshotImage.height ||
      goldenImage.width != screenshotImage.width) {
    print(
      'The golden images have a different height or width. '
      'Golden: ${goldenImage.sizeDisplay}\n'
      'Screenshot: ${screenshotImage.sizeDisplay}\n',
    );
    return _defaultDiffPercentage;
  }

  final goldenImageBytes = goldenImage.getBytes();
  final screenshotImageBytes = screenshotImage.getBytes();
  if (goldenImageBytes.length != screenshotImageBytes.length) {
    print(
      'The golden images have a different byte lengths. '
      'Golden: ${goldenImageBytes.length} bytes\n'
      'Screenshot: ${screenshotImageBytes.length} bytes\n',
    );
    return _defaultDiffPercentage;
  }

  // This image diff calculation code is used by the Flutter test matcher
  // [matchesReferenceImage]. The small bit of code copied here is pulled out
  // for convenient reuse.
  int delta = 0;
  for (int i = 0; i < goldenImageBytes.length; i += 4) {
    if (goldenImageBytes[i] != screenshotImageBytes[i] ||
        goldenImageBytes[i + 1] != screenshotImageBytes[i + 1] ||
        goldenImageBytes[i + 2] != screenshotImageBytes[i + 2] ||
        goldenImageBytes[i + 3] != screenshotImageBytes[i + 3]) {
      delta++;
    }
  }
  return delta / goldenImageBytes.length / 4;
}

extension _ImageExtension on Image {
  String get sizeDisplay =>
      'Size(width: ${width.toDouble()}, height: ${height.toDouble()})';
}
